#!/usr/bin/python
# -*- coding: UTF-8 -*-

import os
import re
import logging
import sys
import getopt
import time
import argparse
import subprocess

_COMPILE_OPTIONS_CHECKER_VERSION = '0.1.3'

# c/c++ compiler name under linux
# for example, aarch64-linux-gnu-gcc, aarch64-linux-gnu-g++, arm-linux-gnueabi-gcc, arm-linux-gnueabi-g++, mips-openwrt-linux-gcc, arm-eabi-gcc
g_linux_compiler = ['gcc', 'g++']

# c/c++ compiler name under vxworks
g_vxworks_compiler = ['ccppc', 'ccarm', 'ccmips']

# c/c++ linker name under linux
# for example, aarch64-linux-gnu-ar, arm-linux-gnueabi-ar, mips-openwrt-linux-ar, arm-eabi-ar
g_linux_linker = ['ar']

# c/c++ linker name under vxworks
g_vxworks_linker = ['arppc', 'ararm', 'armips', 'ldppc', 'ldarm', 'ldmips']

# windriver linux 4.3 + MIPS not support '-fstack-protector'
# windriver linux 4.3 not support '-Wl,-z,noexecstack' 
# windriver linux 6 + MIPS not support '-Wl,-z,noexecstack'
# if need hot/cold path, please ignore "-fPIE -pie"
# need ignore options under linux
g_linux_ignore_options = []

# vxworks 6.8 not support '-Wl,-z,noexecstack'
# need ignore options under linux
g_vxworks_ignore_options = []

def main():
	args = parse_args()
	update_args(args)
	
	if os.path.isfile(args.file_paths):
		check_log_file(args.file_paths)
	elif os.path.isdir(args.file_paths):
		for root, dirs, files in os.walk(args.file_paths):
			for file in files:
				log_file = os.path.join(root, file)
				check_log_file(log_file)
	else:
		print('Invalid file path.\n')
	return

def parse_args():
	parser = argparse.ArgumentParser(description='this is a compile options checker')
	parser.add_argument('--version', action='version', version=_COMPILE_OPTIONS_CHECKER_VERSION)
	parser.add_argument('--file-paths', '-fp', required=True, help='files need to be checked')
	parser.add_argument('--linux-compiler', '-lc', help='compilers for linux, separated by colon')
	parser.add_argument('--linux-linker', '-ll', help='linkers for linux, separated by colon')
	parser.add_argument('--linux-ignore-options', '-lio', help='ignore options for linux, separated by colon')
	parser.add_argument('--vxworks-compiler', '-vc', help='compilers for vxworks, separated by colon')   
	parser.add_argument('--vxworks-linker', '-vl', help='linkers for vxworks, separated by colon')
	parser.add_argument('--vxworks-ignore-options', '-vio', help='ignore options for vxworks, separated by colon')
	args = parser.parse_args()
	return args

def update_args(args):	
	if args.linux_compiler != None:
		compilers = args.linux_compiler.split(':')
		for compiler in compilers:
			if compiler not in g_linux_compiler:
				g_linux_compiler.append(compiler)
	
	if args.linux_linker != None:
		linkers = args.linux_linker.split(':')
		for linker in linkers:
			if linker not in g_linux_linker:
				g_linux_linker.append(linker)
			
	if args.linux_ignore_options != None:
		options = args.linux_ignore_options.split(':')
		for option in options:
			index = option.find('-')
			option = option[index:]
			if option not in g_linux_ignore_options:
				g_linux_ignore_options.append(option)

	if args.vxworks_compiler != None:
		compilers = args.vxworks_compiler.split(':')
		for compiler in compilers:
			if compiler not in g_vxworks_compiler:
				g_vxworks_compiler.append(compiler)
	
	if args.vxworks_linker != None:
		linkers = args.vxworks_linker.split(':')
		for linker in linkers:
			if linker not in g_vxworks_linker:
				g_vxworks_linker.append(linker)
	
	if args.vxworks_ignore_options != None:	
		options = args.vxworks_ignore_options.split(':')
		for option in options:
			index = option.find('-')
			option = option[index:]
			if option not in g_vxworks_ignore_options:
				g_vxworks_ignore_options.append(option)
			
	return

def check_log_file(log_file):
	trimmed_file = os.path.splitext(log_file)[0] + '_trimmed.txt'
	result_file = os.path.splitext(log_file)[0] + '_result.txt'
	options_file = os.path.splitext(log_file)[0] + '_options.txt'	
	trim_file(log_file, trimmed_file)	
	check_trimmed_file(trimmed_file, result_file)	
	stat_options(trimmed_file, options_file)
	return

# delete none-compile command
def trim_file(log_file_name, trimmed_file_name):
	file_origin = open(log_file_name, 'r', encoding='UTF-8', errors='ignore')
	file_trimmed = open(trimmed_file_name, 'w', encoding='UTF-8', errors='ignore')
	line_num = 0
	for line in file_origin.readlines(): 
		line_num += 1
		line = line.strip('\n')
		origin_words = line.split(' ')
		if is_build_cmd(origin_words):
			origin_words.insert(0, 'line')
			origin_words.insert(1, '{}'.format(line_num))
			origin_words.insert(2, ':')
			trimmed_words = filter(is_important_option, origin_words)
			write_list(trimmed_words, file_trimmed)
			
	file_origin.close()
	file_trimmed.close()			
	return 

def check_trimmed_file(trimmed_file_name, result_file_name):
	lines = 0
	errors = 0
	
	file_trimmed = open(trimmed_file_name, 'r', encoding='UTF-8', errors='ignore')
	file_result = open(result_file_name, 'w', encoding='UTF-8', errors='ignore')
	
	for line in file_trimmed.readlines(): 
		lines += 1
		
		line = line.strip('\n')
		words = line.split(' ')
		
		# check command line
		if not check_build_cmd(words, file_result):
			errors += 1
	
	# write statistics
	file_result.writelines('A total of {0} commands were checked and errors were found in {1} commands.\n'.format(lines, errors))
	
	file_trimmed.close()	
	file_result.close()
	return

# check compile command line, write errors into file
def check_build_cmd(words, file_result):	
	must_errors = []
	mustnot_errors = []
		
	if with_linux_compiler(words):
		if with_c_option(words) or with_source_file(words):
			check_options_for_compile(words, must_errors, mustnot_errors)
		if with_o_option(words) and with_shared_option(words) and without_c_option(words):
			check_options_for_dynamic_lib(words, must_errors, mustnot_errors)
		if with_o_option(words) and without_shared_option(words) and without_c_option(words) :
			check_options_for_exe(words, must_errors, mustnot_errors)	
	elif with_linux_linker(words):
		check_options_for_static_lib(words, must_errors, mustnot_errors)
	elif with_vxworks_compiler(words):
		check_options_for_vxworks_compile(words, must_errors, mustnot_errors)
	elif with_vxworks_linker(words):
		check_options_for_vxwroks_link(words, must_errors, mustnot_errors)
	else:
		return True
		
	if len(must_errors) == 0 and len(mustnot_errors) == 0:
		return True
		
	write_errors(words, must_errors, mustnot_errors, file_result)	
	return False
	
# check compile options 
# MUST     : -Wall/-fstack-protector/-fPIC
# MUST NOT : -w
def check_options_for_compile(words, must_errors, mustnot_errors):
	must_options = ['-Wall', '-fstack-protector', '-fPIC']
	for option in must_options:
		if option not in g_linux_ignore_options:
			if not is_option_used(option, words):
				must_errors.append(option)
	
	mustnot_options = ['-w']
	for option in mustnot_options:
		if option not in g_linux_ignore_options:
			if option in words:
				mustnot_errors.append(option)
				
	return

# check options for static lib
# MUST     : None
# MUST NOT : None
def check_options_for_static_lib(words, must_errors, mustnot_errors):
	return

# check options for dynamic lib
# MUST     : -Wl,-z,relro/-Wl,-z,noexecstack/-Wl,-z,now  
# MUST NOT : None
def check_options_for_dynamic_lib(words, must_errors, mustnot_errors):
	must_options = ['-Wl,-z,relro', '-Wl,-z,noexecstack', '-Wl,-z,now']
	for option in must_options:
		if option not in g_linux_ignore_options:
			if not is_option_used(option, words):
				must_errors.append(option)				
	return
	
# check options for exe
# MUST     : -Wl,-z,relro/-Wl,-z,noexecstack/-Wl,-z,now/-pie	
# MUST NOT : None
def check_options_for_exe(words, must_errors, mustnot_errors):
	must_options = ['-Wl,-z,relro', '-Wl,-z,noexecstack', '-Wl,-z,now', '-pie']
	for option in must_options:
		if option not in g_linux_ignore_options:
			if not is_option_used(option, words):
				must_errors.append(option)						
	return

# check compile options for vxworks
# MUST     : -Wall
# MUST NOT : -w
def check_options_for_vxworks_compile(words, must_errors, mustnot_errors):	
	must_options = ['-Wall']
	for option in must_options:
		if option not in g_vxworks_ignore_options:
			if option not in words:
				must_errors.append(option)
	
	mustnot_options = ['-w']
	for option in mustnot_options:
		if option not in g_vxworks_ignore_options:
			if option in words:
				mustnot_errors.append(option)
	return

# check link options for vxworks
# MUST     : -Wl,-z,noexecstack	
# MUST NOT : None
def check_options_for_vxwroks_link(words, must_errors, mustnot_errors):
	must_options = ['-Wl,-z,noexecstack']
	for option in must_options:
		if option not in g_vxworks_ignore_options:
			if not is_option_used(option, words):
				must_errors.append(option)			
	return
	
# write errors into file
def write_errors(words, must_errors, mustnot_errors, file):
	# write command line
	write_list(words, file)
	
	# write MUST options
	if len(must_errors) != 0:
		file.writelines('Error : The following compile options must be used. ')
		if '-fstack-protector' in must_errors:
			must_errors.remove('-fstack-protector')
			must_errors.append('-fstack-protector-all or -fstack-protector-strong')
		write_list(must_errors, file)
	
	# write MUST NOT options
	if len(mustnot_errors) != 0:
		file.writelines('Error : The following compile options must not be used. ')
		write_list(mustnot_errors, file)
		
	file.writelines('\n')
	return
	
##############################################################################
	
# connect words into one line, split with space, then write into file
def write_list(words, file):
	for word in words:
		file.writelines(word + ' ')
	file.writelines('\n')
	return
	
def real_option(word):
	if word.startswith('-o'):
		return '-o'
	else:
		return word
			
def merge_options(words, options):
	for word in words:
		if is_option(word) and (not is_lib_include(word)) and (not is_lib_link(word)):
			word = real_option(word)
			if word not in options:
				options.append(word)
	return

def write_options(options, file):
	options.sort()
	
	for option in options:
		file.writelines(option)
		file.writelines('\n')
	return
	
def stat_options(trimmed_file_name, options_file_name):
	options = []
	
	file_trimmed = open(trimmed_file_name, 'r', encoding='UTF-8', errors='ignore')
	file_options = open(options_file_name, 'w', encoding='UTF-8', errors='ignore')
	
	for line in file_trimmed.readlines(): 
		words = line.split(' ')
		merge_options(words, options)
		
	write_options(options, file_options)
	
	file_trimmed.close()	
	file_options.close()
	return
	
##############################################################################	

def is_option(word):
	return word.startswith('-')

# for example, -L/usr1/V8R18C10_TRUNK/RELEASE_V8/svrp/ne5ke/x86/lib
def is_lib_include(word):
	return word.startswith('-L')

# for example, -lappcfgi
def is_lib_link(word):
	return word.startswith('-l')

def is_linux_compiler(word):
	for compiler in g_linux_compiler:
		if word.endswith(compiler) and not word.startswith('-'):
			return True
	return False

def is_vxworks_compiler(word):
	for compiler in g_vxworks_compiler:
		if word.endswith(compiler) and not word.startswith('-'):
			return True
	return False
	
def is_compiler(word):
	return is_linux_compiler(word) or is_vxworks_compiler(word)

def with_linux_compiler(words):
	for word in words:
		if is_linux_compiler(word):
			return True
	return False

def with_vxworks_compiler(words):
	for word in words:
		if is_vxworks_compiler(word):
			return True
	return False
	
def with_compiler(words):
	return with_linux_compiler(words) or with_vxworks_compiler(words)
	
def is_linux_linker(word):
	for compiler in g_linux_linker:
		if word.endswith(compiler) and not word.startswith('-'):
			return True
	return False
	
def is_vxworks_linker(word):
	for compiler in g_vxworks_linker:
		if word.endswith(compiler) and not word.startswith('-'):
			return True
	return False
	
def is_linker(word):
	return is_linux_linker(word) or is_vxworks_linker(word)

def with_linux_linker(words):
	for word in words:
		if is_linux_linker(word):
			return True
	return False

def with_vxworks_linker(words):
	for word in words:
		if is_vxworks_linker(word):
			return True
	return False
	
def with_linker(words):
	return with_linux_linker(words) or with_vxworks_linker(words)
	
def is_include_option(word):
	return word.startswith('-I')

def is_important_option(word):
	return not is_include_option(word)

def is_relro_option(word):
	return word.startswith('-Wl,') and '-z,relro' in word
		
def is_noexecstack_option(word):
	return word.startswith('-Wl,') and '-z,noexecstack' in word

def is_now_option(word):
	return word.startswith('-Wl,') and '-z,now' in word
	
# -fstack-protector-all -fstack-protector-strong
def is_stackprotector_option(word):
	options = ['-fstack-protector-all', '-fstack-protector-strong']
	return word in options
	
def is_relro_used(words):	
	for word in words:
		if is_relro_option(word):
			return True
	return False
	
def is_noexecstack_used(words):
	for word in words:
		if is_noexecstack_option(word):
			return True
	return False

def	is_stackprotector_used(words):
	for word in words:
		if is_stackprotector_option(word):
			return True
	return False

def	is_now_used(words):
	for word in words:
		if is_now_option(word):
			return True
	return False
	
def is_option_used(option, words):
	if option == '-Wl,-z,relro':
		return is_relro_used(words)
	elif option == '-Wl,-z,noexecstack':
		return is_noexecstack_used(words)
	elif option == '-Wl,-z,now':
		return is_now_used(words)
	elif option == '-fstack-protector':
		return is_stackprotector_used(words)		
	else:
		return option in words
		
def with_c_option(words):
	return '-c' in words

def without_c_option(words):
	return '-c' not in words
	
def with_o_option(words):
	for word in words:
		if word.startswith('-o'):
			return True
	return False

def without_o_option(words):
	return not with_o_option(words)
	
def with_shared_option(words):
	return ('-shared' in words) or ('--shared' in words)

def without_shared_option(words):
	return ('-shared' not in words) and ('--shared' not in words)
	
def with_source_file(words):
	for word in words:
		if word.endswith('.c') or word.endswith('.cpp'):
			return True
	return False

def with_obj_file(words):
	for word in words:
		if word.endswith('.o') or word.endswith('.obj'):
			return True
	return False

def with_lib_file(words):
	for word in words:
		if word.endswith('.a') or word.endswith('.lib'):
			return True
	return False
	
def is_compile_cmd(words):
	return with_compiler(words) and (with_c_option(words) or with_o_option(words) or with_source_file(words))

def is_link_cmd(words):
	return with_linker(words) and with_obj_file(words) and with_lib_file(words)

def is_build_cmd(words):
	return is_compile_cmd(words) or is_link_cmd(words)

##############################################################################		

	
if __name__ == '__main__':
	main()
	
	


    
	
	
